package com.ddw.meedeeuh.shared.movie.database;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import com.almworks.sqlite4java.SQLiteConnection;
import com.almworks.sqlite4java.SQLiteException;
import com.almworks.sqlite4java.SQLiteQueue;
import com.almworks.sqlite4java.SQLiteStatement;
import com.ddw.meedeeuh.shared.database.DatabaseCallback;
import com.ddw.meedeeuh.shared.database.DatabaseException;
import com.ddw.meedeeuh.shared.database.DatabaseVersionException;
import com.ddw.meedeeuh.shared.database.QueryParamHelper;
import com.ddw.meedeeuh.shared.database.sqlite.SQLiteJobHelper;
import com.ddw.meedeeuh.shared.database.sqlite.SQLiteQueueAdapter;
import com.ddw.meedeeuh.shared.database.sqlite.SQLiteUtils;
import com.ddw.meedeeuh.shared.io.SourceFile;
import com.ddw.meedeeuh.shared.io.SourceFileFactory;
import com.ddw.meedeeuh.shared.io.SourceFolder;
import com.ddw.meedeeuh.shared.io.SourceFolderFactory;
import com.ddw.meedeeuh.shared.movie.Actor;
import com.ddw.meedeeuh.shared.movie.Audio;
import com.ddw.meedeeuh.shared.movie.ExtraThumb;
import com.ddw.meedeeuh.shared.movie.Movie;
import com.ddw.meedeeuh.shared.movie.StreamDetails;
import com.ddw.meedeeuh.shared.movie.Subtitle;
import com.ddw.meedeeuh.shared.movie.Video;

class MovieDBSQLite implements MovieDB {

	private static final Logger LOGGER = Logger.getLogger(MovieDBSQLite.class.getName());
	private static final int EXPECTED_VERSION = 1;
	private final SQLiteQueueAdapter queue;

	public MovieDBSQLite(File file) {
		this.queue = new MovieSQLiteQueueAdapter(file);
	}

	@Override
	public Future<Map<Integer, DBMovie>> movies(final DatabaseCallback<Map<Integer, DBMovie>> callback) {
		Future<Map<Integer, DBMovie>> future = null;
		try {
			future = getQueue().execute(new SQLiteJobHelper<Map<Integer, DBMovie>>(callback, "movies", LOGGER) {

				@Override
				protected Map<Integer, DBMovie> job(SQLiteConnection db) throws Throwable {
					// Create statement for sql lite
					SQLiteStatement st = null;

					// Fetch all the actors
					final Map<Integer, Map<Integer, DBActor>> actors = new HashMap<Integer, Map<Integer, DBActor>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(ActorTable.ACTOR_ID);
						params.addParam(ActorTable.MOVIE_ID);
						params.addParam(ActorTable.NAME);
						params.addParam(ActorTable.ROLE);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + ActorTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int actorId = SQLiteUtils.getInt(st, params.index(ActorTable.ACTOR_ID));
							final int movieId = SQLiteUtils.getInt(st, params.index(ActorTable.MOVIE_ID));
							final String name = SQLiteUtils.getString(st, params.index(ActorTable.NAME));
							final String role = SQLiteUtils.getNullableString(st, params.index(ActorTable.ROLE));

							// Append actor to map indexed by movie id > actor id
							if(actors.containsKey(movieId) == false) {
								actors.put(movieId, new HashMap<Integer, DBActor>());
							}
							actors.get(movieId).put(actorId, DBActorFactory.create(actorId, movieId, name, role));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading actors from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the videos
					final Map<Integer, Map<Integer, DBVideo>> videos = new HashMap<Integer, Map<Integer, DBVideo>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(VideoTable.VIDEO_ID);
						params.addParam(VideoTable.STREAM_DETAILS_ID);
						params.addParam(VideoTable.ASPECT);
						params.addParam(VideoTable.CODEC);
						params.addParam(VideoTable.WIDTH);
						params.addParam(VideoTable.HEIGHT);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + VideoTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int videoId = SQLiteUtils.getInt(st, params.index(VideoTable.VIDEO_ID));
							final int streamDetailsId = SQLiteUtils.getInt(st, params.index(VideoTable.STREAM_DETAILS_ID));
							final Double aspect = SQLiteUtils.getNullableDouble(st, params.index(VideoTable.ASPECT));
							final String codec = SQLiteUtils.getNullableString(st, params.index(VideoTable.CODEC));
							final Integer width = SQLiteUtils.getNullableInt(st, params.index(VideoTable.WIDTH));
							final Integer height = SQLiteUtils.getNullableInt(st, params.index(VideoTable.HEIGHT));

							// Append video to map indexed by stream details id > video id
							if(videos.containsKey(streamDetailsId) == false) {
								videos.put(streamDetailsId, new HashMap<Integer, DBVideo>());
							}
							videos.get(streamDetailsId).put(videoId, DBVideoFactory.create(videoId, streamDetailsId, aspect, codec, width, height));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading videos from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the audios
					final Map<Integer, Map<Integer, DBAudio>> audios = new HashMap<Integer, Map<Integer, DBAudio>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(AudioTable.AUDIO_ID);
						params.addParam(AudioTable.STREAM_DETAILS_ID);
						params.addParam(AudioTable.CHANNELS);
						params.addParam(AudioTable.CODEC);
						params.addParam(AudioTable.LANGUAGE);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + AudioTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int audioId = SQLiteUtils.getInt(st, params.index(AudioTable.AUDIO_ID));
							final int streamDetailsId = SQLiteUtils.getInt(st, params.index(AudioTable.STREAM_DETAILS_ID));
							final Integer channels = SQLiteUtils.getNullableInt(st, params.index(AudioTable.CHANNELS));
							final String codec = SQLiteUtils.getNullableString(st, params.index(AudioTable.CODEC));
							final String language = SQLiteUtils.getNullableString(st, params.index(AudioTable.LANGUAGE));

							// Append audio to map indexed by stream details id > audio id
							if(audios.containsKey(streamDetailsId) == false) {
								audios.put(streamDetailsId, new HashMap<Integer, DBAudio>());
							}
							audios.get(streamDetailsId).put(audioId, DBAudioFactory.create(audioId, streamDetailsId, channels, codec, language));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading audios from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the subtitles
					final Map<Integer, Map<Integer, DBSubtitle>> subtitles = new HashMap<Integer, Map<Integer, DBSubtitle>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(SubtitleTable.SUBTITLE_ID);
						params.addParam(SubtitleTable.STREAM_DETAILS_ID);
						params.addParam(SubtitleTable.LANGUAGE);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + SubtitleTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int subtitleId = SQLiteUtils.getInt(st, params.index(SubtitleTable.SUBTITLE_ID));
							final int streamDetailsId = SQLiteUtils.getInt(st, params.index(SubtitleTable.STREAM_DETAILS_ID));
							final String language = SQLiteUtils.getNullableString(st, params.index(SubtitleTable.LANGUAGE));

							// Append subtitle to map indexed by stream details id > subtitle id
							if(subtitles.containsKey(streamDetailsId) == false) {
								subtitles.put(streamDetailsId, new HashMap<Integer, DBSubtitle>());
							}
							subtitles.get(streamDetailsId).put(subtitleId, DBSubtitleFactory.create(subtitleId, streamDetailsId, language));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading subtitles from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the stream details
					final Map<Integer, Map<Integer, DBStreamDetails>> streamDetails = new HashMap<Integer, Map<Integer, DBStreamDetails>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(StreamDetailTable.STREAM_DETAILS_ID);
						params.addParam(StreamDetailTable.MOVIE_ID);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + StreamDetailTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int streamDetailsId = SQLiteUtils.getInt(st, params.index(StreamDetailTable.STREAM_DETAILS_ID));
							final int movieId = SQLiteUtils.getInt(st, params.index(StreamDetailTable.MOVIE_ID));

							// Get the videos for this stream details or a new map if empty
							final Map<Integer, DBVideo> streamDetailsVideos = videos.containsKey(streamDetails) ?
									videos.get(streamDetails) : new HashMap<Integer, DBVideo>();

							// Get the audios for this stream details or a new map if empty
							final Map<Integer, DBAudio> streamDetailsAudios = audios.containsKey(streamDetails) ?
									audios.get(streamDetails) : new HashMap<Integer, DBAudio>();

							// Get the subtitles for this stream details or a new map if empty
							final Map<Integer, DBSubtitle> streamDetailsSubtitles = subtitles.containsKey(streamDetails) ?
									subtitles.get(streamDetails) : new HashMap<Integer, DBSubtitle>();

							// Append stream details to map indexed by movie id > stream details id
							if(streamDetails.containsKey(movieId) == false) {
								streamDetails.put(movieId, new HashMap<Integer, DBStreamDetails>());
							}
							streamDetails.get(movieId).put(streamDetailsId, DBStreamDetailsFactory.create(streamDetailsId, movieId,
									streamDetailsVideos, streamDetailsAudios, streamDetailsSubtitles));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading stream details from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the extra thumbs
					final Map<Integer, Map<Integer, DBExtraThumb>> extraThumbs = new HashMap<Integer, Map<Integer, DBExtraThumb>>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(ExtraThumbTable.EXTRATHUMB_ID);
						params.addParam(ExtraThumbTable.MOVIE_ID);
						params.addParam(ExtraThumbTable.EXTRATHUMB_PATH);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + ExtraThumbTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int extraThumbId = SQLiteUtils.getInt(st, params.index(ExtraThumbTable.EXTRATHUMB_ID));
							final int movieId = SQLiteUtils.getInt(st, params.index(ExtraThumbTable.MOVIE_ID));
							final String extraThumbPath = SQLiteUtils.getString(st, params.index(ExtraThumbTable.EXTRATHUMB_PATH));

							// Append extra thumb to map indexed by movie id
							if(extraThumbs.containsKey(movieId) == false) {
								extraThumbs.put(movieId, new HashMap<Integer, DBExtraThumb>());
							}
							extraThumbs.get(movieId).put(extraThumbId, DBExtraThumbFactory.create(extraThumbId, movieId, extraThumbPath));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading extra thumbs from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					// Fetch all the movies
					final Map<Integer, DBMovie> movies = new HashMap<Integer, DBMovie>();
					try {
						// Create parameters list
						final QueryParamHelper params = new QueryParamHelper();
						params.addParam(MovieTable.MOVIE_ID);
						params.addParam(MovieTable.CREDITS);
						params.addParam(MovieTable.DIRECTOR);
						params.addParam(MovieTable.GENRE);
						params.addParam(MovieTable.IMDB_ID);
						params.addParam(MovieTable.MOVIE_SET);
						params.addParam(MovieTable.MPAA);
						params.addParam(MovieTable.ORIGINAL_TITLE);
						params.addParam(MovieTable.OUTLINE);
						params.addParam(MovieTable.FOLDER_PATH);
						params.addParam(MovieTable.PLAY_COUNT);
						params.addParam(MovieTable.PLOT);
						params.addParam(MovieTable.RATING);
						params.addParam(MovieTable.RUNTIME);
						params.addParam(MovieTable.SORT_TITLE);
						params.addParam(MovieTable.TAGLINE);
						params.addParam(MovieTable.THUMBNAIL);
						params.addParam(MovieTable.TITLE);
						params.addParam(MovieTable.YEAR);
						params.addParam(MovieTable.TOP250);
						params.addParam(MovieTable.TRAILER);
						params.addParam(MovieTable.VOTES);
						params.addParam(MovieTable.WATCHED);
						params.addParam(MovieTable.NFO_PATH);
						params.addParam(MovieTable.POSTER_PATH);
						params.addParam(MovieTable.FANART_PATH);

						// Execute the sql query
						st = db.prepare("SELECT " + params.getSelectQuery() + " FROM " + MovieTable.TABLE_NAME);

						// Read rows
						while(st.step()) {
							final int movieId = SQLiteUtils.getInt(st, params.index(MovieTable.MOVIE_ID));
							final String name = SQLiteUtils.getString(st, params.index(MovieTable.TITLE));
							final Integer year = SQLiteUtils.getNullableInt(st, params.index(MovieTable.YEAR));
							final String originalTitle = SQLiteUtils.getNullableString(st, params.index(MovieTable.ORIGINAL_TITLE));
							final String sortTitle = SQLiteUtils.getNullableString(st, params.index(MovieTable.SORT_TITLE));
							final String set = SQLiteUtils.getNullableString(st, params.index(MovieTable.MOVIE_SET));
							final Double rating = SQLiteUtils.getNullableDouble(st, params.index(MovieTable.RATING));
							final Boolean top250 = SQLiteUtils.getNullableBoolean(st, params.index(MovieTable.TOP250));
							final Integer votes = SQLiteUtils.getNullableInt(st, params.index(MovieTable.VOTES));
							final String outline = SQLiteUtils.getNullableString(st, params.index(MovieTable.OUTLINE));
							final String plot = SQLiteUtils.getNullableString(st, params.index(MovieTable.PLOT));
							final String tagline = SQLiteUtils.getNullableString(st, params.index(MovieTable.TAGLINE));
							final Integer runtime = SQLiteUtils.getNullableInt(st, params.index(MovieTable.RUNTIME));
							final String thumbnail = SQLiteUtils.getNullableString(st, params.index(MovieTable.THUMBNAIL));
							final String mpaa = SQLiteUtils.getNullableString(st, params.index(MovieTable.MPAA));
							final Integer playCount = SQLiteUtils.getNullableInt(st, params.index(MovieTable.PLAY_COUNT));
							final String imdbId = SQLiteUtils.getNullableString(st, params.index(MovieTable.IMDB_ID));
							final String folderPath = SQLiteUtils.getString(st, params.index(MovieTable.FOLDER_PATH));
							final SourceFolder folder = SourceFolderFactory.create(folderPath);
							final String trailer = SQLiteUtils.getNullableString(st, params.index(MovieTable.TRAILER));
							final String genre = SQLiteUtils.getNullableString(st, params.index(MovieTable.GENRE));
							final String credits = SQLiteUtils.getNullableString(st, params.index(MovieTable.CREDITS));
							final String director = SQLiteUtils.getNullableString(st, params.index(MovieTable.DIRECTOR));
							final Boolean watched = SQLiteUtils.getNullableBoolean(st, params.index(MovieTable.WATCHED));
							final String nfoPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.NFO_PATH));
							final SourceFile nfo = nfoPath == null ? null : SourceFileFactory.create(nfoPath);
							final String posterPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.POSTER_PATH));
							final SourceFile poster = posterPath == null ? null : SourceFileFactory.create(posterPath);
							final String fanartPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.FANART_PATH));
							final SourceFile fanart = fanartPath == null ? null : SourceFileFactory.create(fanartPath);

							// Get the actors for this movie or a new map if empty
							final Map<Integer, DBActor> movieActors = actors.containsKey(movieId) ?
									actors.get(movieId) : new HashMap<Integer, DBActor>();

							// Get the stream details for this movie or a new map if empty
							final Map<Integer, DBStreamDetails> movieStreamDetails = streamDetails.containsKey(movieId) ?
									streamDetails.get(movieId) : new HashMap<Integer, DBStreamDetails>();

							// Get the extra thumbs for this movie or a new map if empty
							final Map<Integer, DBExtraThumb> movieExtraThumbs = extraThumbs.containsKey(movieId) ?
									extraThumbs.get(movieId) : new HashMap<Integer, DBExtraThumb>();

							movies.put(movieId, DBMovieFactory.create(movieId, name, year, originalTitle, sortTitle, set, rating,
									top250, votes, outline, plot, tagline, runtime, thumbnail, mpaa, playCount, imdbId,
									folder, trailer, genre, credits, director, watched, movieStreamDetails, movieActors,
									nfo, poster, fanart, movieExtraThumbs));
						}
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading movies from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}

					return movies;
				}

			});
		} catch (Throwable e) {
			callback.onFailure(e);
		}
		return future;
	}

	@Override
	public Future<DBMovie> movie(final int movieId, DatabaseCallback<DBMovie> callback) {
		Future<DBMovie> future = null;
		try {
			future = getQueue().execute(new SQLiteJobHelper<DBMovie>(callback, "movie", LOGGER) {

				@Override
				protected DBMovie job(SQLiteConnection db) throws Throwable {
					return movie(movieId, db);
				}

			});
		} catch (Throwable e) {
			callback.onFailure(e);
		}
		return future;
	}

	private DBMovie movie(int movieId, SQLiteConnection db) throws DatabaseException {
		// Create statement for sql lite
		SQLiteStatement st = null;

		// Fetch all the actors
		final Map<Integer, DBActor> actors = new HashMap<Integer, DBActor>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(ActorTable.ACTOR_ID);
			params.addParam(ActorTable.NAME);
			params.addParam(ActorTable.ROLE);

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " +
					ActorTable.TABLE_NAME + " WHERE " + ActorTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int actorId = SQLiteUtils.getInt(st, params.index(ActorTable.ACTOR_ID));
				final String name = SQLiteUtils.getString(st, params.index(ActorTable.NAME));
				final String role = SQLiteUtils.getNullableString(st, params.index(ActorTable.ROLE));

				// Append actor to map indexed by actor id
				actors.put(actorId, DBActorFactory.create(actorId, movieId, name, role));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading actors from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch all the videos
		final Map<Integer, DBVideo> videos = new HashMap<Integer, DBVideo>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(VideoTable.VIDEO_ID, "v");
			params.addParam(VideoTable.STREAM_DETAILS_ID, "v");
			params.addParam(VideoTable.ASPECT, "v");
			params.addParam(VideoTable.CODEC, "v");
			params.addParam(VideoTable.WIDTH, "v");
			params.addParam(VideoTable.HEIGHT, "v");

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " +
					StreamDetailTable.TABLE_NAME + " s INNER JOIN " + VideoTable.TABLE_NAME +
					" v ON s." + StreamDetailTable.STREAM_DETAILS_ID + " = v." + VideoTable.STREAM_DETAILS_ID +
					" WHERE s." + StreamDetailTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int videoId = SQLiteUtils.getInt(st, params.index(VideoTable.VIDEO_ID));
				final int streamDetailsId = SQLiteUtils.getInt(st, params.index(VideoTable.STREAM_DETAILS_ID));
				final Double aspect = SQLiteUtils.getNullableDouble(st, params.index(VideoTable.ASPECT));
				final String codec = SQLiteUtils.getNullableString(st, params.index(VideoTable.CODEC));
				final Integer width = SQLiteUtils.getNullableInt(st, params.index(VideoTable.WIDTH));
				final Integer height = SQLiteUtils.getNullableInt(st, params.index(VideoTable.HEIGHT));

				// Append video to map indexed by video id
				videos.put(videoId, DBVideoFactory.create(videoId, streamDetailsId, aspect, codec, width, height));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading videos from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch all the audios
		final Map<Integer, DBAudio> audios = new HashMap<Integer, DBAudio>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(AudioTable.AUDIO_ID, "a");
			params.addParam(AudioTable.STREAM_DETAILS_ID, "a");
			params.addParam(AudioTable.CHANNELS, "a");
			params.addParam(AudioTable.CODEC, "a");
			params.addParam(AudioTable.LANGUAGE, "a");

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " +
					StreamDetailTable.TABLE_NAME + " s INNER JOIN " + AudioTable.TABLE_NAME +
					" a ON s." + StreamDetailTable.STREAM_DETAILS_ID + " = a." + AudioTable.STREAM_DETAILS_ID +
					" WHERE s." + StreamDetailTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int audioId = SQLiteUtils.getInt(st, params.index(AudioTable.AUDIO_ID));
				final int streamDetailsId = SQLiteUtils.getInt(st, params.index(AudioTable.STREAM_DETAILS_ID));
				final Integer channels = SQLiteUtils.getNullableInt(st, params.index(AudioTable.CHANNELS));
				final String codec = SQLiteUtils.getNullableString(st, params.index(AudioTable.CODEC));
				final String language = SQLiteUtils.getNullableString(st, params.index(AudioTable.LANGUAGE));

				// Append audio to map indexed by audio id
				audios.put(audioId, DBAudioFactory.create(audioId, streamDetailsId, channels, codec, language));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading audios from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch all the subtitles
		final Map<Integer, DBSubtitle> subtitles = new HashMap<Integer, DBSubtitle>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(SubtitleTable.SUBTITLE_ID, "t");
			params.addParam(SubtitleTable.STREAM_DETAILS_ID, "t");
			params.addParam(SubtitleTable.LANGUAGE, "t");

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " +
					StreamDetailTable.TABLE_NAME + " s INNER JOIN " + SubtitleTable.TABLE_NAME +
					" t ON s." + StreamDetailTable.STREAM_DETAILS_ID + " = t." + SubtitleTable.STREAM_DETAILS_ID +
					" WHERE s." + StreamDetailTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int subtitleId = SQLiteUtils.getInt(st, params.index(SubtitleTable.SUBTITLE_ID));
				final int streamDetailsId = SQLiteUtils.getInt(st, params.index(SubtitleTable.STREAM_DETAILS_ID));
				final String language = SQLiteUtils.getNullableString(st, params.index(SubtitleTable.LANGUAGE));

				// Append subtitle to map indexed by subtitle id
				subtitles.put(subtitleId, DBSubtitleFactory.create(subtitleId, streamDetailsId, language));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading subtitles from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch all the stream details
		final Map<Integer, DBStreamDetails> streamDetails = new HashMap<Integer, DBStreamDetails>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(StreamDetailTable.STREAM_DETAILS_ID);

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " + StreamDetailTable.TABLE_NAME +
					" WHERE " + StreamDetailTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int streamDetailsId = SQLiteUtils.getInt(st, params.index(StreamDetailTable.STREAM_DETAILS_ID));

				// Append stream details to map indexed by stream details id
				streamDetails.put(streamDetailsId, DBStreamDetailsFactory.create(streamDetailsId, movieId,
						videos, audios, subtitles));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading stream details from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch all the extra thumbs
		final Map<Integer, DBExtraThumb> extraThumbs = new HashMap<Integer, DBExtraThumb>();
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(ExtraThumbTable.EXTRATHUMB_ID);
			params.addParam(ExtraThumbTable.EXTRATHUMB_PATH);

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " + ExtraThumbTable.TABLE_NAME +
					" WHERE " + ExtraThumbTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read rows
			while(st.step()) {
				final int extraThumbId = SQLiteUtils.getInt(st, params.index(ExtraThumbTable.EXTRATHUMB_ID));
				final String extraThumbPath = SQLiteUtils.getString(st, params.index(ExtraThumbTable.EXTRATHUMB_PATH));

				// Append extra thumb to map indexed by extra thumb id
				extraThumbs.put(extraThumbId, DBExtraThumbFactory.create(extraThumbId, movieId, extraThumbPath));
			}
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading extra thumbs from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}

		// Fetch the movie
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(MovieTable.CREDITS);
			params.addParam(MovieTable.DIRECTOR);
			params.addParam(MovieTable.GENRE);
			params.addParam(MovieTable.IMDB_ID);
			params.addParam(MovieTable.MOVIE_SET);
			params.addParam(MovieTable.MPAA);
			params.addParam(MovieTable.ORIGINAL_TITLE);
			params.addParam(MovieTable.OUTLINE);
			params.addParam(MovieTable.FOLDER_PATH);
			params.addParam(MovieTable.PLAY_COUNT);
			params.addParam(MovieTable.PLOT);
			params.addParam(MovieTable.RATING);
			params.addParam(MovieTable.RUNTIME);
			params.addParam(MovieTable.SORT_TITLE);
			params.addParam(MovieTable.TAGLINE);
			params.addParam(MovieTable.THUMBNAIL);
			params.addParam(MovieTable.TITLE);
			params.addParam(MovieTable.YEAR);
			params.addParam(MovieTable.TOP250);
			params.addParam(MovieTable.TRAILER);
			params.addParam(MovieTable.VOTES);
			params.addParam(MovieTable.WATCHED);
			params.addParam(MovieTable.NFO_PATH);
			params.addParam(MovieTable.POSTER_PATH);
			params.addParam(MovieTable.FANART_PATH);

			// Execute the sql query
			final String sql = "SELECT " + params.getSelectQuery() + " FROM " +
					MovieTable.TABLE_NAME + " WHERE " + MovieTable.MOVIE_ID + " = ?";
			st = db.prepare(sql);
			st.bind(1, movieId);

			// Read the movie row
			if(st.step() == false) {
				throw new DatabaseException("Error reading movie, the movie does not exist");
			}

			final String name = SQLiteUtils.getString(st, params.index(MovieTable.TITLE));
			final Integer year = SQLiteUtils.getNullableInt(st, params.index(MovieTable.YEAR));
			final String originalTitle = SQLiteUtils.getNullableString(st, params.index(MovieTable.ORIGINAL_TITLE));
			final String sortTitle = SQLiteUtils.getNullableString(st, params.index(MovieTable.SORT_TITLE));
			final String set = SQLiteUtils.getNullableString(st, params.index(MovieTable.MOVIE_SET));
			final Double rating = SQLiteUtils.getNullableDouble(st, params.index(MovieTable.RATING));
			final Boolean top250 = SQLiteUtils.getNullableBoolean(st, params.index(MovieTable.TOP250));
			final Integer votes = SQLiteUtils.getNullableInt(st, params.index(MovieTable.VOTES));
			final String outline = SQLiteUtils.getNullableString(st, params.index(MovieTable.OUTLINE));
			final String plot = SQLiteUtils.getNullableString(st, params.index(MovieTable.PLOT));
			final String tagline = SQLiteUtils.getNullableString(st, params.index(MovieTable.TAGLINE));
			final Integer runtime = SQLiteUtils.getNullableInt(st, params.index(MovieTable.RUNTIME));
			final String thumbnail = SQLiteUtils.getNullableString(st, params.index(MovieTable.THUMBNAIL));
			final String mpaa = SQLiteUtils.getNullableString(st, params.index(MovieTable.MPAA));
			final Integer playCount = SQLiteUtils.getNullableInt(st, params.index(MovieTable.PLAY_COUNT));
			final String imdbId = SQLiteUtils.getNullableString(st, params.index(MovieTable.IMDB_ID));
			final String folderPath = SQLiteUtils.getString(st, params.index(MovieTable.FOLDER_PATH));
			final SourceFolder folder = SourceFolderFactory.create(folderPath);
			final String trailer = SQLiteUtils.getNullableString(st, params.index(MovieTable.TRAILER));
			final String genre = SQLiteUtils.getNullableString(st, params.index(MovieTable.GENRE));
			final String credits = SQLiteUtils.getNullableString(st, params.index(MovieTable.CREDITS));
			final String director = SQLiteUtils.getNullableString(st, params.index(MovieTable.DIRECTOR));
			final Boolean watched = SQLiteUtils.getNullableBoolean(st, params.index(MovieTable.WATCHED));
			final String nfoPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.NFO_PATH));
			final SourceFile nfo = nfoPath == null ? null : SourceFileFactory.create(nfoPath);
			final String posterPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.POSTER_PATH));
			final SourceFile poster = posterPath == null ? null : SourceFileFactory.create(posterPath);
			final String fanartPath = SQLiteUtils.getNullableString(st, params.index(MovieTable.FANART_PATH));
			final SourceFile fanart = fanartPath == null ? null : SourceFileFactory.create(fanartPath);

			return DBMovieFactory.create(movieId, name, year, originalTitle, sortTitle, set, rating,
					top250, votes, outline, plot, tagline, runtime, thumbnail, mpaa, playCount, imdbId,
					folder, trailer, genre, credits, director, watched, streamDetails, actors,
					nfo, poster, fanart, extraThumbs);
		} catch (SQLiteException e) {
			throw new DatabaseException("Error reading movie from database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	@Override
	public Future<Boolean> deleteMovies(DatabaseCallback<Boolean> callback) {
		Future<Boolean> future = null;
		try {
			future = getQueue().execute(new SQLiteJobHelper<Boolean>(callback, "delete movies", LOGGER) {

				@Override
				protected Boolean job(SQLiteConnection db) throws Throwable {
					setJournalMode(db);

					// Delete all the actors
					try {
						// Execute the sql query
						final StringBuilder sql = new StringBuilder();
						sql.append("DELETE FROM " + ExtraThumbTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + ActorTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + VideoTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + AudioTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + SubtitleTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + StreamDetailTable.TABLE_NAME + ";");
						sql.append("DELETE FROM " + MovieTable.TABLE_NAME + ";");
						db.exec(sql.toString());
					} catch (SQLiteException e) {
						throw new DatabaseException("Error deleting all movie data from database", e);
					}
					return true;
				}

			});
		} catch (Throwable e) {
			callback.onFailure(e);
		}
		return future;
	}

	@Override
	public Future<Boolean> deleteMovie(final int movieId, DatabaseCallback<Boolean> callback) {
		Future<Boolean> future = null;
		try {
			future = getQueue().execute(new SQLiteJobHelper<Boolean>(callback, "delete movie", LOGGER) {

				@Override
				protected Boolean job(SQLiteConnection db) throws Throwable {
					setJournalMode(db);
					deleteMovie(movieId, db);
					return true;
				}

			});
		} catch (Throwable e) {
			callback.onFailure(e);
		}
		return future;
	}

	@Override
	public Future<DBMovie> setMovie(final Movie movie, DatabaseCallback<DBMovie> callback) {
		Future<DBMovie> future = null;
		try {
			future = getQueue().execute(new SQLiteJobHelper<DBMovie>(callback, "set movie", LOGGER) {

				@Override
				protected DBMovie job(SQLiteConnection db) throws Throwable {
					// Check if the movie is already a database movie
					final boolean isDbMovie = movie instanceof DBMovie;
					Integer movieId = isDbMovie ? ((DBMovie)movie).getMovieId() : null;

					setJournalMode(db);

					// Lock the database for writing and start a transaction
					db.exec("BEGIN IMMEDIATE");

					try {
						// Delete the movie and all referenced records in
						// actors, stream details, audio, video, subtitles etc.
						if(isDbMovie) {
							deleteMovie(movieId, db);
						}

						// Save the movie
						movieId = setMovie(db, movie);

						// Check we have a valid movie id (this shouldn't happen)
						if(movieId == null) {
							throw new IllegalStateException("Invalid state, movie id was null");
						}

						// Save actors
						for(final Actor actor : movie.getActors()) {
							setActor(db, movieId, actor);
						}

						// Save stream details
						for(final StreamDetails streamDetails : movie.getStreamDetails()) {
							final int streamDetailsId = setStreamDetails(db, movieId, streamDetails);

							// Save video details
							for(final Video video : streamDetails.getVideos()) {
								setVideo(db, streamDetailsId, video);
							}

							// Save audio details
							for(final Audio audio : streamDetails.getAudios()) {
								setAudio(db, streamDetailsId, audio);
							}

							// Save subtitle details
							for(final Subtitle subtitle : streamDetails.getSubtitles()) {
								setSubtitle(db, streamDetailsId, subtitle);
							}
						}

						// Save extra thumbs
						for(final ExtraThumb extraThumb : movie.getExtraThumbs()) {
							setExtraThumb(db, movieId, extraThumb);
						}
					} catch (Throwable e) {
						// Rollback the transaction
						db.exec("ROLLBACK");

						// Rethrow the exception
						throw e;
					}

					// Commit the transaction if successful
					db.exec("COMMIT");

					// Read the saved movie
					return movie(movieId, db);
				}

			});
		} catch (Throwable e) {
			callback.onFailure(e);
		}
		return future;
	}

	@Override
	public void create(DatabaseCallback<Boolean> callback) {
		try {
			if(this.queue.isQueueCached()) {
				throw new DatabaseException("Database queue exists (is cached), cannot recreate");
			}

			getQueue();
			callback.onSuccess(true);
		} catch (DatabaseVersionException e) {
			callback.onFailure(e);
		} catch (DatabaseException e) {
			callback.onFailure(e);
		}
	}

	@Override
	public void exec(final String sql) throws DatabaseVersionException, DatabaseException {
		try {
			getQueue().execute(new SQLiteJobHelper<Boolean>(null, "exec", LOGGER) {

				@Override
				protected Boolean job(SQLiteConnection connection) throws Throwable {
					connection.exec(sql);
					return true;
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	@Override
	public int execInt(final String sql) throws DatabaseVersionException, DatabaseException {
		try {
			return getQueue().execute(new SQLiteJobHelper<Integer>(null, "exec int", LOGGER) {

				@Override
				protected Integer job(SQLiteConnection connection) throws Throwable {
					// Create statement for sql lite
					SQLiteStatement st = null;

					// Execute query and return value
					try {
						st = connection.prepare(sql);
						if(st.step() == false) {
							throw new DatabaseException("Error reading int value, no results");
						}
						return st.columnInt(0);
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading int value from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	@Override
	public double execDouble(final String sql) throws DatabaseVersionException, DatabaseException {
		try {
			return getQueue().execute(new SQLiteJobHelper<Double>(null, "exec double", LOGGER) {

				@Override
				protected Double job(SQLiteConnection connection) throws Throwable {
					// Create statement for sql lite
					SQLiteStatement st = null;

					// Execute query and return value
					try {
						st = connection.prepare(sql);
						if(st.step() == false) {
							throw new DatabaseException("Error reading double value, no results");
						}
						return st.columnDouble(0);
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading double value from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	@Override
	public String execString(final String sql) throws DatabaseVersionException, DatabaseException {
		try {
			return getQueue().execute(new SQLiteJobHelper<String>(null, "exec string", LOGGER) {

				@Override
				protected String job(SQLiteConnection connection) throws Throwable {
					// Create statement for sql lite
					SQLiteStatement st = null;

					// Execute query and return value
					try {
						st = connection.prepare(sql);
						if(st.step() == false) {
							throw new DatabaseException("Error reading string value, no results");
						}
						return st.columnString(0);
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading string value from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	@Override
	public boolean execBoolean(final String sql) throws DatabaseVersionException, DatabaseException {
		try {
			return getQueue().execute(new SQLiteJobHelper<Boolean>(null, "exec boolean", LOGGER) {

				@Override
				protected Boolean job(SQLiteConnection connection) throws Throwable {
					// Create statement for sql lite
					SQLiteStatement st = null;

					// Execute query and return value
					try {
						st = connection.prepare(sql);
						if(st.step() == false) {
							throw new DatabaseException("Error reading boolean value, no results");
						}
						return st.columnInt(0) == 1;
					} catch (SQLiteException e) {
						throw new DatabaseException("Error reading boolean value from database", e);
					} finally {
						if(st != null) {
							st.dispose();
						}
					}
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	@Override
	public void clean() throws DatabaseVersionException, DatabaseException {
		try {
			getQueue().execute(new SQLiteJobHelper<Boolean>(null, "clean", LOGGER) {

				@Override
				protected Boolean job(SQLiteConnection connection) throws Throwable {
					connection.exec("VACUUM");
					return true;
				}

			}).get();
		} catch (InterruptedException e) {
			throw new DatabaseException("Query was interrupted", e);
		} catch (ExecutionException e) {
			throw new DatabaseException("Failed to execute query", e);
		}
	}

	private void deleteMovie(final int movieId, SQLiteConnection db) throws DatabaseException {
		// Delete all the actors
		try {
			// Execute the sql query
			final StringBuilder sql = new StringBuilder();
			sql.append(String.format("DELETE FROM " + ExtraThumbTable.TABLE_NAME + " WHERE " + ExtraThumbTable.MOVIE_ID + " = %d ;", movieId));
			sql.append(String.format("DELETE FROM " + ActorTable.TABLE_NAME + " WHERE " + ActorTable.MOVIE_ID + " = %d ;", movieId));
			sql.append(String.format("DELETE FROM " + VideoTable.TABLE_NAME + " WHERE " + VideoTable.STREAM_DETAILS_ID + " IN (" +
					" SELECT " + StreamDetailTable.STREAM_DETAILS_ID + " FROM " + StreamDetailTable.TABLE_NAME +
					" WHERE " + StreamDetailTable.MOVIE_ID + " = %d );", movieId));
			sql.append(String.format("DELETE FROM " + AudioTable.TABLE_NAME + " WHERE " + AudioTable.STREAM_DETAILS_ID + " IN (" +
					" SELECT " + StreamDetailTable.STREAM_DETAILS_ID + " FROM " + StreamDetailTable.TABLE_NAME +
					" WHERE " + StreamDetailTable.MOVIE_ID + " = %d );", movieId));
			sql.append(String.format("DELETE FROM " + SubtitleTable.TABLE_NAME + " WHERE " + SubtitleTable.STREAM_DETAILS_ID + " IN (" +
					" SELECT " + StreamDetailTable.STREAM_DETAILS_ID + " FROM " + StreamDetailTable.TABLE_NAME +
					" WHERE " + StreamDetailTable.MOVIE_ID + " = %d );", movieId));
			sql.append(String.format("DELETE FROM " + StreamDetailTable.TABLE_NAME + " WHERE " + StreamDetailTable.MOVIE_ID + " = %d ;", movieId));
			sql.append(String.format("DELETE FROM " + MovieTable.TABLE_NAME + " WHERE " + MovieTable.MOVIE_ID + " = %d;", movieId));
			db.exec(sql.toString());
		} catch (SQLiteException e) {
			throw new DatabaseException("Error deleting all movie data from database", e);
		}
	}

	private int setMovie(SQLiteConnection db, Movie movie) throws DatabaseException {
		// Check if the movie is already a database movie
		final boolean isDbMovie = movie instanceof DBMovie;
		Integer movieId = isDbMovie ? ((DBMovie)movie).getMovieId() : null;

		// Create statement for sql lite
		SQLiteStatement st = null;

		// Save movie
		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			params.addParam(MovieTable.MOVIE_ID);
			params.addParam(MovieTable.CREDITS);
			params.addParam(MovieTable.DIRECTOR);
			params.addParam(MovieTable.GENRE);
			params.addParam(MovieTable.IMDB_ID);
			params.addParam(MovieTable.MOVIE_SET);
			params.addParam(MovieTable.MPAA);
			params.addParam(MovieTable.ORIGINAL_TITLE);
			params.addParam(MovieTable.OUTLINE);
			params.addParam(MovieTable.FOLDER_PATH);
			params.addParam(MovieTable.PLAY_COUNT);
			params.addParam(MovieTable.PLOT);
			params.addParam(MovieTable.RATING);
			params.addParam(MovieTable.RUNTIME);
			params.addParam(MovieTable.SORT_TITLE);
			params.addParam(MovieTable.TAGLINE);
			params.addParam(MovieTable.THUMBNAIL);
			params.addParam(MovieTable.TITLE);
			params.addParam(MovieTable.YEAR);
			params.addParam(MovieTable.TOP250);
			params.addParam(MovieTable.TRAILER);
			params.addParam(MovieTable.VOTES);
			params.addParam(MovieTable.WATCHED);
			params.addParam(MovieTable.NFO_PATH);
			params.addParam(MovieTable.POSTER_PATH);
			params.addParam(MovieTable.FANART_PATH);

			// Build the sql query
			final String sql = "INSERT INTO " + MovieTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbMovie) {
				st.bind(params.bindIndex(MovieTable.MOVIE_ID), movieId.intValue());
			}

			st.bind(params.bindIndex(MovieTable.FOLDER_PATH), movie.getFolder().getInstance().getPath());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.CREDITS), movie.getCredits());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.DIRECTOR), movie.getDirector());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.GENRE), movie.getGenre());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.IMDB_ID), movie.getImdbId());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.MOVIE_SET), movie.getSet());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.MPAA), movie.getMpaa());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.ORIGINAL_TITLE), movie.getOriginalTitle());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.OUTLINE), movie.getOutline());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.PLAY_COUNT), movie.getPlayCount());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.PLOT), movie.getPlot());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.RATING), movie.getRating());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.RUNTIME), movie.getRuntime());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.SORT_TITLE), movie.getSortTitle());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.TAGLINE), movie.getTagLine());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.THUMBNAIL), movie.getThumbnail());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.TITLE), movie.getName());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.YEAR), movie.getYear());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.TOP250), movie.isTop250());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.TRAILER), movie.getTrailer());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.VOTES), movie.getVotes());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.WATCHED), movie.isWatched());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.NFO_PATH), movie.getNfo() == null ? null : movie.getNfo().getInstance().getPath());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.POSTER_PATH), movie.getPoster() == null ? null : movie.getPoster().getInstance().getPath());
			SQLiteUtils.bindNullable(st, params.bindIndex(MovieTable.FANART_PATH), movie.getFanart() == null ? null : movie.getFanart().getInstance().getPath());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the movie id if it was auto generated
			return isDbMovie ? movieId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving movie to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setSubtitle(SQLiteConnection db, int streamDetailsId, Subtitle subtitle) throws SQLiteException, DatabaseException {
		// Check if the subtitle is already a database subtitle
		final boolean isDbSubtitle = subtitle instanceof DBSubtitle;
		final Integer subtitleId = isDbSubtitle ? ((DBSubtitle)subtitle).getSubtitleId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbSubtitle) {
				params.addParam(SubtitleTable.SUBTITLE_ID);
			}
			params.addParam(SubtitleTable.STREAM_DETAILS_ID);
			params.addParam(SubtitleTable.LANGUAGE);

			// Build the sql query
			final String sql = "INSERT INTO " + SubtitleTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbSubtitle) {
				st.bind(params.bindIndex(SubtitleTable.SUBTITLE_ID), subtitleId.intValue());
			}
			st.bind(params.bindIndex(SubtitleTable.STREAM_DETAILS_ID), streamDetailsId);
			SQLiteUtils.bindNullable(st, params.bindIndex(SubtitleTable.LANGUAGE), subtitle.getLanguage());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the subtitle id if it was auto generated
			return isDbSubtitle ? subtitleId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving subtitle to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setAudio(SQLiteConnection db, int streamDetailsId, Audio audio) throws SQLiteException, DatabaseException {
		// Check if the audio is already a database audio
		final boolean isDbAudio = audio instanceof DBAudio;
		final Integer audioId = isDbAudio ? ((DBAudio)audio).getAudioId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbAudio) {
				params.addParam(AudioTable.AUDIO_ID);
			}
			params.addParam(AudioTable.STREAM_DETAILS_ID);
			params.addParam(AudioTable.CHANNELS);
			params.addParam(AudioTable.CODEC);
			params.addParam(AudioTable.LANGUAGE);

			// Build the sql query
			final String sql = "INSERT INTO " + AudioTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbAudio) {
				st.bind(params.bindIndex(AudioTable.AUDIO_ID), audioId.intValue());
			}
			st.bind(params.bindIndex(AudioTable.STREAM_DETAILS_ID), streamDetailsId);
			SQLiteUtils.bindNullable(st, params.bindIndex(AudioTable.CHANNELS), audio.getChannels());
			SQLiteUtils.bindNullable(st, params.bindIndex(AudioTable.CODEC), audio.getCodec());
			SQLiteUtils.bindNullable(st, params.bindIndex(AudioTable.LANGUAGE), audio.getLanguage());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the audio id if it was auto generated
			return isDbAudio ? audioId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving audio to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setVideo(SQLiteConnection db, int streamDetailsId, Video video) throws SQLiteException, DatabaseException {
		// Check if the video is already a database video
		final boolean isDbVideo = video instanceof DBVideo;
		final Integer videoId = isDbVideo ? ((DBVideo)video).getVideoId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbVideo) {
				params.addParam(VideoTable.VIDEO_ID);
			}
			params.addParam(VideoTable.STREAM_DETAILS_ID);
			params.addParam(VideoTable.ASPECT);
			params.addParam(VideoTable.CODEC);
			params.addParam(VideoTable.HEIGHT);
			params.addParam(VideoTable.WIDTH);

			// Build the sql query
			final String sql = "INSERT INTO " + VideoTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbVideo) {
				st.bind(params.bindIndex(VideoTable.VIDEO_ID), videoId.intValue());
			}
			st.bind(params.bindIndex(VideoTable.STREAM_DETAILS_ID), streamDetailsId);
			SQLiteUtils.bindNullable(st, params.bindIndex(VideoTable.ASPECT), video.getAspect());
			SQLiteUtils.bindNullable(st, params.bindIndex(VideoTable.CODEC), video.getCodec());
			SQLiteUtils.bindNullable(st, params.bindIndex(VideoTable.HEIGHT), video.getHeight());
			SQLiteUtils.bindNullable(st, params.bindIndex(VideoTable.WIDTH), video.getWidth());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the video id if it was auto generated
			return isDbVideo ? videoId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving video to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setStreamDetails(SQLiteConnection db, int movieId, StreamDetails streamDetails) throws SQLiteException, DatabaseException {
		// Check if the stream details is already a database stream details
		final boolean isDbStreamDetails = streamDetails instanceof DBStreamDetails;
		final Integer streamDetailsId = isDbStreamDetails ? ((DBStreamDetails)streamDetails).getStreamDetailsId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbStreamDetails) {
				params.addParam(StreamDetailTable.STREAM_DETAILS_ID);
			}
			params.addParam(StreamDetailTable.MOVIE_ID);

			// Build the sql query
			final String sql = "INSERT INTO " + StreamDetailTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbStreamDetails) {
				st.bind(params.bindIndex(StreamDetailTable.STREAM_DETAILS_ID), streamDetailsId.intValue());
			}
			st.bind(params.bindIndex(StreamDetailTable.MOVIE_ID), movieId);

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the stream details id if it was auto generated
			return isDbStreamDetails ? streamDetailsId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving stream details to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setActor(SQLiteConnection db, int movieId, Actor actor) throws SQLiteException, DatabaseException {
		// Check if the actor is already a database actor
		final boolean isDbActor = actor instanceof DBActor;
		final Integer actorId = isDbActor ? ((DBActor)actor).getActorId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbActor) {
				params.addParam(ActorTable.ACTOR_ID);
			}
			params.addParam(ActorTable.MOVIE_ID);
			params.addParam(ActorTable.NAME);
			params.addParam(ActorTable.ROLE);

			// Build the sql query
			final String sql = "INSERT INTO " + ActorTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbActor) {
				st.bind(params.bindIndex(ActorTable.ACTOR_ID), actorId.intValue());
			}
			st.bind(params.bindIndex(ActorTable.MOVIE_ID), movieId);
			SQLiteUtils.bindNullable(st, params.bindIndex(ActorTable.NAME), actor.getName());
			SQLiteUtils.bindNullable(st, params.bindIndex(ActorTable.ROLE), actor.getRole());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the actor id if it was auto generated
			return isDbActor ? actorId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving actor to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private int setExtraThumb(SQLiteConnection db, int movieId, ExtraThumb extraThumb) throws SQLiteException, DatabaseException {
		// Check if the extra thumb is already a database extra thumb
		final boolean isDbExtraThumb = extraThumb instanceof DBExtraThumb;
		final Integer extraThumbId = isDbExtraThumb ? ((DBExtraThumb)extraThumb).getExtraThumbId() : null;

		// Create sqlite statement
		SQLiteStatement st = null;

		try {
			// Create parameters list
			final QueryParamHelper params = new QueryParamHelper();
			if(isDbExtraThumb) {
				params.addParam(ExtraThumbTable.EXTRATHUMB_ID);
			}
			params.addParam(ExtraThumbTable.MOVIE_ID);
			params.addParam(ExtraThumbTable.EXTRATHUMB_PATH);

			// Build the sql query
			final String sql = "INSERT INTO " + ExtraThumbTable.TABLE_NAME + " ( " +
					params.getSelectQuery() + " ) VALUES ( " + getBindParamsString(params) + " )";
			st = db.prepare(sql);
			if(isDbExtraThumb) {
				st.bind(params.bindIndex(ExtraThumbTable.EXTRATHUMB_ID), extraThumbId.intValue());
			}
			st.bind(params.bindIndex(ExtraThumbTable.MOVIE_ID), movieId);
			st.bind(params.bindIndex(ExtraThumbTable.EXTRATHUMB_PATH), extraThumb.getInstance().getPath());

			// Check the right number of bound parameters
			if(st.getBindParameterCount() != params.getParameterCount()) {
				throw new DatabaseException("The number of bind parameters does not match the number of input parameters");
			}

			// Execute the statement
			st.stepThrough();

			// Set the extra thumb id if it was auto generated
			return isDbExtraThumb ? extraThumbId : (int) db.getLastInsertId();
		} catch (SQLiteException e) {
			throw new DatabaseException("Error saving extra thumb to database", e);
		} finally {
			if(st != null) {
				st.dispose();
			}
		}
	}

	private static String getBindParamsString(QueryParamHelper params) {
		final StringBuilder builder = new StringBuilder();
		for(int i = 0, s = params.getParameterCount(); i < s; i++) {
			builder.append(i > 0 ? ", ?" : "?");
		}
		return builder.toString();
	}

	private SQLiteQueue getQueue() throws DatabaseVersionException, DatabaseException {
		return this.queue.getQueue(EXPECTED_VERSION);
	}

	private static void setJournalMode(SQLiteConnection db) throws SQLiteException {
		db.exec("PRAGMA journal_mode = MEMORY"); // Optimization for transactions
	}

	private static enum MovieTable {

		MOVIE_ID("movieid"),
		FOLDER_PATH("folderpath"),
		TITLE("title"),
		ORIGINAL_TITLE("originaltitle"),
		SORT_TITLE("sorttitle"),
		MOVIE_SET("movieset"),
		RATING("rating"),
		YEAR("year"),
		TOP250("top250"),
		VOTES("votes"),
		OUTLINE("outline"),
		PLOT("plot"),
		TAGLINE("tagline"),
		RUNTIME("runtime"),
		THUMBNAIL("thumbnail"),
		MPAA("mpaa"),
		PLAY_COUNT("playcount"),
		WATCHED("watched"),
		IMDB_ID("imdbid"),
		TRAILER("trailer"),
		GENRE("genre"),
		CREDITS("credits"),
		DIRECTOR("director"),
		NFO_PATH("nfopath"),
		POSTER_PATH("posterpath"),
		FANART_PATH("fanartpath");

		public static final String TABLE_NAME = "movie";
		private final String fieldName;

		MovieTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum ExtraThumbTable {

		EXTRATHUMB_ID("extrathumbid"),
		MOVIE_ID("movieid"),
		EXTRATHUMB_PATH("extrathumbpath");

		public static final String TABLE_NAME = "extrathumb";
		private final String fieldName;

		ExtraThumbTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum ActorTable {

		ACTOR_ID("actorid"),
		MOVIE_ID("movieid"),
		NAME("name"),
		ROLE("role");

		public static final String TABLE_NAME = "actor";
		private final String fieldName;

		ActorTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum StreamDetailTable {

		STREAM_DETAILS_ID("streamdetailsid"),
		MOVIE_ID("movieid");

		public static final String TABLE_NAME = "streamdetails";
		private final String fieldName;

		StreamDetailTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum VideoTable {

		VIDEO_ID("videoid"),
		STREAM_DETAILS_ID("streamdetailsid"),
		CODEC("codec"),
		ASPECT("aspect"),
		WIDTH("width"),
		HEIGHT("height");

		public static final String TABLE_NAME = "video";
		private final String fieldName;

		VideoTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum AudioTable {

		AUDIO_ID("audioid"),
		STREAM_DETAILS_ID("streamdetailsid"),
		CODEC("codec"),
		LANGUAGE("language"),
		CHANNELS("channels");

		public static final String TABLE_NAME = "audio";
		private final String fieldName;

		AudioTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static enum SubtitleTable {

		SUBTITLE_ID("subtitleid"),
		STREAM_DETAILS_ID("streamdetailsid"),
		LANGUAGE("language");

		public static final String TABLE_NAME = "subtitle";
		private final String fieldName;

		SubtitleTable(String fieldName) {
			this.fieldName = fieldName;
		}

		@Override
		public String toString() {
			return getFieldName();
		}

		public String getFieldName() {
			return this.fieldName;
		}

	}

	private static class MovieSQLiteQueueAdapter extends SQLiteQueueAdapter {

		public MovieSQLiteQueueAdapter(File file) {
			super(file, LOGGER);
		}

		@Override
		public void onCreate(SQLiteConnection connection) throws DatabaseException {
			try {
				String sql = "CREATE TABLE " + MovieTable.TABLE_NAME + " ( " +
						MovieTable.MOVIE_ID + " INTEGER NOT NULL, " +
						MovieTable.FOLDER_PATH + " TEXT NOT NULL, " +
						MovieTable.TITLE + " TEXT NOT NULL, " +
						MovieTable.ORIGINAL_TITLE + " TEXT, " +
						MovieTable.SORT_TITLE + " TEXT, " +
						MovieTable.MOVIE_SET + " TEXT, " +
						MovieTable.RATING + " REAL, " +
						MovieTable.YEAR + " INTEGER, " +
						MovieTable.TOP250 + " INTEGER, " +
						MovieTable.VOTES + " INTEGER, " +
						MovieTable.OUTLINE + " TEXT, " +
						MovieTable.PLOT + " TEXT, " +
						MovieTable.TAGLINE + " TEXT, " +
						MovieTable.RUNTIME + " INTEGER, " +
						MovieTable.THUMBNAIL + " TEXT, " +
						MovieTable.MPAA + " TEXT, " +
						MovieTable.PLAY_COUNT + " INTEGER, " +
						MovieTable.WATCHED + " INTEGER, " +
						MovieTable.IMDB_ID + " TEXT, " +
						MovieTable.TRAILER + " TEXT, " +
						MovieTable.GENRE + " TEXT, " +
						MovieTable.CREDITS + " TEXT, " +
						MovieTable.DIRECTOR + " TEXT, " +
						MovieTable.NFO_PATH + " TEXT, " +
						MovieTable.POSTER_PATH + " TEXT, " +
						MovieTable.FANART_PATH + " TEXT, " +
						"PRIMARY KEY (" + MovieTable.MOVIE_ID + ") " +
						")";
				connection.exec(sql);
				sql = "CREATE INDEX movieidxfolderpath ON " + MovieTable.TABLE_NAME + " (" + MovieTable.FOLDER_PATH + ")";
				connection.exec(sql);
				sql = "CREATE INDEX movieidxtitle ON " + MovieTable.TABLE_NAME + " (" + MovieTable.TITLE + ")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create movie table", e);
			}

			try {
				String sql = "CREATE TABLE " + ActorTable.TABLE_NAME + " ( " +
						ActorTable.ACTOR_ID + " INTEGER NOT NULL, " +
						ActorTable.MOVIE_ID + " INTEGER NOT NULL, " +
						ActorTable.NAME + " TEXT NOT NULL, " +
						ActorTable.ROLE + " TEXT, " +
						"PRIMARY KEY (" + ActorTable.ACTOR_ID + "), " +
						"FOREIGN KEY (" + ActorTable.MOVIE_ID + ") REFERENCES " + MovieTable.TABLE_NAME + " (" + MovieTable.MOVIE_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create actor table", e);
			}

			try {
				String sql = "CREATE TABLE " + StreamDetailTable.TABLE_NAME + " ( " +
						StreamDetailTable.STREAM_DETAILS_ID + " INTEGER NOT NULL, " +
						StreamDetailTable.MOVIE_ID + " INTEGER NOT NULL, " +
						"PRIMARY KEY (" + StreamDetailTable.STREAM_DETAILS_ID + "), " +
						"FOREIGN KEY (" + StreamDetailTable.MOVIE_ID + ") REFERENCES " + MovieTable.TABLE_NAME + " (" + MovieTable.MOVIE_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create stream details table", e);
			}

			try {
				String sql = "CREATE TABLE " + VideoTable.TABLE_NAME + " ( " +
						VideoTable.VIDEO_ID + " INTEGER NOT NULL, " +
						VideoTable.STREAM_DETAILS_ID + " INTEGER NOT NULL, " +
						VideoTable.CODEC + " TEXT, " +
						VideoTable.ASPECT + " REAL, " +
						VideoTable.WIDTH + " INTEGER, " +
						VideoTable.HEIGHT + " INTEGER, " +
						"PRIMARY KEY (" + VideoTable.VIDEO_ID + "), " +
						"FOREIGN KEY (" + VideoTable.STREAM_DETAILS_ID + ") REFERENCES " + StreamDetailTable.TABLE_NAME + " (" + StreamDetailTable.STREAM_DETAILS_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create video table", e);
			}

			try {
				String sql = "CREATE TABLE " + AudioTable.TABLE_NAME + " ( " +
						AudioTable.AUDIO_ID + " INTEGER NOT NULL, " +
						AudioTable.STREAM_DETAILS_ID + " INTEGER NOT NULL, " +
						AudioTable.CODEC + " TEXT, " +
						AudioTable.LANGUAGE + " TEXT, " +
						AudioTable.CHANNELS + " INTEGER, " +
						"PRIMARY KEY (" + AudioTable.AUDIO_ID + "), " +
						"FOREIGN KEY (" + AudioTable.STREAM_DETAILS_ID + ") REFERENCES " + StreamDetailTable.TABLE_NAME + " (" + StreamDetailTable.STREAM_DETAILS_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create audio table", e);
			}

			try {
				String sql = "CREATE TABLE " + SubtitleTable.TABLE_NAME + " ( " +
						SubtitleTable.SUBTITLE_ID + " INTEGER NOT NULL, " +
						SubtitleTable.STREAM_DETAILS_ID + " INTEGER NOT NULL, " +
						SubtitleTable.LANGUAGE + " TEXT, " +
						"PRIMARY KEY (" + SubtitleTable.SUBTITLE_ID + "), " +
						"FOREIGN KEY (" + SubtitleTable.STREAM_DETAILS_ID + ") REFERENCES " + StreamDetailTable.TABLE_NAME + " (" + StreamDetailTable.STREAM_DETAILS_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create subtitle table", e);
			}

			try {
				String sql = "CREATE TABLE " + ExtraThumbTable.TABLE_NAME + " ( " +
						ExtraThumbTable.EXTRATHUMB_ID + " INTEGER NOT NULL, " +
						ExtraThumbTable.MOVIE_ID + " INTEGER NOT NULL, " +
						ExtraThumbTable.EXTRATHUMB_PATH + " TEXT NOT NULL, " +
						"PRIMARY KEY (" + ExtraThumbTable.EXTRATHUMB_ID + "), " +
						"FOREIGN KEY (" + ExtraThumbTable.MOVIE_ID + ") REFERENCES " + MovieTable.TABLE_NAME + " (" + MovieTable.MOVIE_ID + ")" +
						")";
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create actor table", e);
			}

			try {
				String sql = "CREATE TABLE version ( version INTEGER NOT NULL, PRIMARY KEY (version) )";
				connection.exec(sql);
				sql = String.format("INSERT INTO version (version) VALUES (%d)", EXPECTED_VERSION);
				connection.exec(sql);
			} catch (SQLiteException e) {
				throw new DatabaseException("Failed to create subtitle table", e);
			}
		}

	}

}
